/*
 *  tcp.c  --  telnet protocol communication module for powwow
 *
 *  Copyright (C) 1998,2002 by Massimiliano Ghilardi
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/time.h>
#include <errno.h>
#ifndef MCLIENT
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <arpa/telnet.h>
#endif

#ifndef TELOPT_NAWS
#  define TELOPT_NAWS 31
#endif
#ifndef NEXT
#  include <unistd.h>
#endif

#ifdef TERM
#  include "client.h"
#endif

#ifdef MCLIENT
# include "PowwowWrapper.h"
#endif

#include "defines.h"
#include "main.h"
#include "utils.h"
#include "tcp.h"
#include "tty.h"
#include "edit.h"
#include "beam.h"
#include "log.h"

#ifdef TELOPTS
# define TELOPTSTR(n) ((n) > NTELOPTS ? "unknown" : telopts[n])
#endif

int tcp_fd = -1;                   /* current socket file descriptor
                                    * -1 means no socket */
int tcp_main_fd = -1;              /* socket file descriptor of main connect.
                                    * -1 means no socket */
int tcp_max_fd = 0;                /* highest used fd */

int tcp_count = 0;                 /* number of open connections */
int tcp_attachcount = 0;           /* number of spawned commands */

int conn_max_index;                /* 1 + highest used conn_list[] index */

connsess conn_list[MAX_CONNECTS];    /* connection list */

byte conn_table[MAX_FDSCAN];         /* fd -> index translation table */

#ifndef MCLIENT
fd_set fdset;                   /* set of descriptors to select() on */
#endif


/*
 * process suboptions.
 * so far, only terminal type is processed but future extensions are
 * window size, X display location, etc.
 */
static void dosubopt __P1 (byte *,str)
{
    char buf[256], *term;
    int len, err;

    if (str[0] == TELOPT_TTYPE) {
        if (str[1] == 1) {
            /* 1 == SEND */
#ifdef TELOPTS
            tty_printf("[got SB TERMINAL TYPE SEND]\n");
#endif
            if (!(term = getenv("TERM"))) term = "unknown";
            sprintf(buf, "%c%c%c%c%.*s%c%c", IAC, SB, TELOPT_TTYPE, 0,
                    256-7, term, IAC, SE);      /* 0 == IS */
            
            len = strlen(term) + 6;
#ifndef MCLIENT
            while ((err = write(tcp_fd, buf, len)) < 0 && errno == EINTR);
#else
            err = wrapper_tcp_write(tcp_fd, buf, len);
#endif
            if (err != len) {
                errmsg("write subopt to socket");
                return;
            }
#ifdef TELOPTS
            tty_printf("[sent SB TERMINAL TYPE IS %s]\n", term);
#endif
        }
    }
}

/*
 * send an option negotiation
 * 'what' is one of WILL, WONT, DO, DONT
 */
static void sendopt __P2 (byte,what, byte,opt)
{
    static byte buf[3] = { IAC, 0, 0 };
    int i;
    buf[1] = what; buf[2] = opt;
    
#ifndef MCLIENT
    while ((i = write(tcp_fd, buf, 3)) < 0 && errno == EINTR);
#else
    i = wrapper_tcp_write(tcp_fd, buf, 3);
#endif
    if (i != 3) {
        errmsg("write option to socket");
        return;
    }

#ifdef TELOPTS
    tty_printf("[sent %s %s]\n", (what == WILL) ? "WILL" :
               (what == WONT) ? "WONT" :
               (what == DO) ? "DO" : (what == DONT) ? "DONT" : "error",
               TELOPTSTR(opt));
#endif
}

#ifndef MCLIENT
/*
 * connect to remote host
 * Warning: some voodoo code here
 */
int tcp_connect __P2 (char *,addr, int,port)
{
    struct sockaddr_in address;
    struct hostent *host_info;
    int err, newtcp_fd;
    
    status(1);
    
    memzero((char *)&address, sizeof(address));
    /*
     * inet_addr has a strange design: It is documented to return -1 for
     * malformed requests, but it is declared to return unsigned long!
     * Anyway, this works.
     */    
    
#ifndef TERM  
    address.sin_addr.s_addr = inet_addr(addr);
    if (address.sin_addr.s_addr != (unsigned int)-1)
        address.sin_family = AF_INET;
    else
    {
        if (opt_info)
            tty_printf("#looking up %s... ", addr);
        tty_flush();
        host_info = gethostbyname(addr);
        if (host_info == 0) {
            if (!opt_info) {
                tty_printf("#looking up %s... ", addr);
            }
            tty_printf("unknown host!\n");
            return -1;
        }
        memmove((char *)&address.sin_addr, host_info->h_addr,
              host_info->h_length);
        address.sin_family = host_info->h_addrtype;
        if (opt_info)
            tty_puts("found.\n");
    }
    address.sin_port = htons(port);
    
    newtcp_fd = socket(address.sin_family, SOCK_STREAM, 0);
    if (newtcp_fd == -1) {
        errmsg("create socket");
        return -1;
    } else if (newtcp_fd >= MAX_FDSCAN) {
        tty_printf("#connect: #error: too many open connections\n");
        close(newtcp_fd);
        return -1;
    }
    
    tty_printf("#trying %s... ", addr);
    tty_flush();
    
    err = connect(newtcp_fd, (struct sockaddr *)&address, sizeof(address));
    
    if (err == -1) { /* CTRL-C pressed, or other errors */
        errmsg("connect to host");
        close(newtcp_fd);
        return -1;
    }
    
#else /* term */
    
    if ((newtcp_fd = connect_server(0)) < 0) {
        tty_puts("\n#powwow: unable to connect to term server\n");
        return -1;
    } else {
        if (newtcp_fd >= MAX_FDSCAN) {
            tty_printf("#connect: #error: too many open connections\n");
            close(newtcp_fd);
            return -1;
        }
        send_command(newtcp_fd, C_PORT, 0, "%s:%d", addr, port);
        tty_puts("Connected to term server...\n");
#ifdef TERM_COMPRESS
        send_command(newtcp_fd, C_COMPRESS, 1, "y");
#endif
        send_command(newtcp_fd, C_DUMB, 1, 0);
    }

#endif /* term */
    
    tty_puts("connected!\n");
    
    
    {
        /*
         * Now set some options on newtcp_fd :
         * First, no-nagle
         */
        int opt = 1;
#       ifndef SOL_TCP
#        define SOL_TCP IPPROTO_TCP
#       endif
        if (setsockopt(newtcp_fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)))
            errmsg("setsockopt(TCP_NODELAY) failed");
        
        /*
         * Then, close-on-exec:
         * we don't want children to inherit the socket!
         */
        
        fcntl(newtcp_fd, F_SETFD, FD_CLOEXEC);
    }
    
    return newtcp_fd;
}
#endif

/*
 * we don't expect IAC commands here, except IAC IAC (a protected ASCII 255)
 * which we replace with a single IAC (a plain ASCII 255)
 */
int tcp_unIAC __P2 (char *,buffer, int,len)
{
    char *s, *start, warnIACs = 1;
    if (!memchr(buffer, IAC, len))
        return len;
    
    for (s = start = buffer; len > 0; buffer++, len--) {
        if (buffer[0] == (char)(byte)IAC) {
            if (len > 1 && buffer[1] == (char)(byte)IAC)
                buffer++, len--;
            else if (warnIACs) {
                PRINTF("#warning: received IAC inside MPI message, treating as IAC IAC.\n");
                warnIACs = 0;
            }
        }
        *s++ = *buffer;
    }
    return s - start;
}

/*
 * the reverse step: protect ASCII 255 as IAC IAC
 * the dest buffer is assumed to be big enough to hold the whole data
 */
static int tcp_addIAC __P3 (char *,dest, char *,txt, int,len)
{
    char *s = dest;
    while (len-- > 0) {
        if ((*s++ = *txt++) == (char)(byte)IAC)
            *s++ = (char)(byte)IAC;
    }
    return s - dest;
}

/*
 * read from an fd, protecting ASCII 255 as IAC IAC while we read.
 * the buffer is assumed to be big enough to hold the whole file
 */
int tcp_read_addIAC __P3 (int,fd, char *,data, int,len)
{
    char *s = data;
    char buf[BUFSIZE];
    int i;
    
    while (len > 0) {
        while ((i = read(fd, buf, MIN2(len, BUFSIZE))) < 0 && errno == EINTR);

        if (i < 0) {
            errmsg("read from file");
            return -1;
        } else if (i == 0)
            break;
        s += tcp_addIAC(s, buf, i);
        len -= i;
    }
    return s - data;
}

/*
 * read a maximum of size chars from remote host
 * using the telnet protocol. return chars read.
 */
int tcp_read __P3 (int,fd, char *,buffer, int,maxsize)
{
  char state = CONN_LIST(fd).state;
  int i;
  static byte subopt[MAX_SUBOPT];
  static int subchars;
  byte *p, *s, *linestart;

#ifndef MCLIENT
  while ((i = read(fd, buffer, maxsize)) < 0 && errno == EINTR);
#else
  i = wrapper_tcp_read(fd, buffer, maxsize);
#endif

  if (i == 0) {
    CONN_LIST(fd).state = NORMAL;
    tcp_close(NULL);
    return 0;
  }
  if (i < 0) {
    errmsg("read from socket");
    return 0;
  }
    
    /*
     * scan through the buffer,
     * interpret telnet protocol escapes and MUME MPI messages
     */
    for (s = p = linestart = (byte *)buffer; i; s++, i--) {
        switch (state) {
         case NORMAL:
         case ALTNORMAL:
         case GOT_R:
         case GOT_N:
            /*
             * Some servers like to send NULs and other funny chars.
             * Clean up as much as possible.
             */
            switch (*s) {
             case IAC:
                state = GOTIAC;
                break;
             case '\r':
                /* start counting \r, unless just got \n */
                if (state == NORMAL || state == ALTNORMAL) {
                    /*
                     * ALTNORMAL is safe here: \r cannot be in MPI header,
                     * and any previous MPI header has already been rejected
                     */
                    state = GOT_R;
                } else if (state == GOT_N)
                  /* after \n\r, we forbid MPI messages */
                    state = ALTNORMAL;
                break;
             case '\n':
                state = GOT_N;
                *p++ = *s;
                linestart = p;
                break;
             case '\0':
                /* skip NULs */
                break;
             default:
                /* first, flush any missing \r */
                if (state == GOT_R)
                  *p++ = '\r';
                
                *p++ = *s;
                
                /* check for MUME MPI messages: */
                if (p - linestart == MPILEN && !memcmp(linestart, MPI, MPILEN)) {
                    if (!(CONN_LIST(fd).flags & IDEDITOR)) {
                        PRINTF("#warning: MPI message received without #request editor!\n");
                    } else if (state == ALTNORMAL) {
                        /* no MPI messages after \n\r */
                        PRINTF("#warning: MPI attack?\n");
                    } else {
                        subchars = process_message((char*)s+1, i-1);
                        /* no +MPILEN here, as it was already processed. */
                        s += subchars; i-= subchars;
                        p = linestart;
                    }
                }
                
                if (state != ALTNORMAL)
                    state = NORMAL;
                break;
            }
            break;
            
         case GOTIAC:
            switch (*s) {
             case WILL:
                state = GOTWILL; break;
             case WONT:
                state = GOTWONT; break;
             case DO:
                state = GOTDO; break;
             case DONT:
                state = GOTDONT; break;
             case SB:           /* BUG (multiple connections):  */
                state = GOTSB;  /* there is only one subopt buffer */
                subchars = 0;
                break;
             case IAC:
                *p++ = IAC;
                state = NORMAL;
                break;
             case GA:
                /* I should handle GA as end-of-prompt marker one day */
                /* one day has come ;) - Max */
                prompt_set_iac((char*)p);
                state = NORMAL;
                break;
             default:
                /* ignore the rest of the telnet commands */
#ifdef TELOPTS
                tty_printf("[skipped IAC <%d>]\n", *s);
#endif
                state = NORMAL;
                break;
            }
            break;
            
         case GOTWILL:
#ifdef TELOPTS
            tty_printf("[got WILL %s]\n", TELOPTSTR(*s));
#endif
            switch(*s) {
             case TELOPT_ECHO:
                /* host echoes, turn off echo here
                 * but only for main connection, since we do not want
                 * subsidiary connection password entries to block anything
                 * in the main connection
                 */
#ifdef MCLIENT
                wrapper_toggle_echo();
#endif
                if (fd == tcp_main_fd)
                    linemode |= LM_NOECHO;
                sendopt(DO, *s);
                break;
             case TELOPT_SGA:
                /* this can't hurt */
                linemode |= LM_CHAR;
#ifndef MCLIENT
                tty_special_keys();
#endif
                sendopt(DO, *s);
                break;
             default:
                /* don't accept other options */
                sendopt(DONT, *s);
                break;
            }
            state = NORMAL;
            break;
            
         case GOTWONT:
#ifdef TELOPTS
            tty_printf("[got WONT %s]\n", TELOPTSTR(*s));
#endif
            if (*s == TELOPT_ECHO) {
                /* host no longer echoes, we do it instead */
#ifdef MCLIENT
              wrapper_toggle_echo();
#endif
                linemode &= ~LM_NOECHO;
            }
            /* accept any WONT */
            sendopt(DONT, *s);
            state = NORMAL;
            break;
            
         case GOTDO:
#ifdef TELOPTS
            tty_printf("[got DO %s]\n", TELOPTSTR(*s));
#endif
            switch(*s) {
             case TELOPT_SGA:
                linemode |= LM_CHAR;
#ifndef MCLIENT
                tty_special_keys();
#endif
                /* FALLTHROUGH */
             case TELOPT_TTYPE:
                sendopt(WILL, *s);
                break;
             case TELOPT_NAWS:
                sendopt(WILL, *s);
                tcp_write_tty_size();
                break;
             default:
                /* accept nothing else */
                sendopt(WONT, *s);
                break;
            }
            state = NORMAL;
            break;
            
         case GOTDONT:
#ifdef TELOPTS
            tty_printf("[got DONT %s]\n", TELOPTSTR(*s));
#endif
            if (*s == TELOPT_SGA) {
                linemode &= ~LM_CHAR;
#ifndef MCLIENT
                tty_special_keys();
#endif
            }
            sendopt(WONT, *s);
            state = NORMAL;
            break;
            
         case GOTSB:
            if (*s == IAC) {
                state = GOTSBIAC;
            } else {
                if (subchars < MAX_SUBOPT)
                  subopt[subchars++] = *s;
            }
            break;
            
         case GOTSBIAC:
            if (*s == IAC) {
                if (subchars < MAX_SUBOPT)
                  subopt[subchars++] = IAC;
                state = GOTSB;
            } else if (*s == SE) {
                subopt[subchars] = '\0';
                dosubopt(subopt);
                state = NORMAL;
            } else {
                /* problem! I haven't the foggiest idea of what to do here.
                 * I'll just ignore it and hope it goes away. */
                PRINTF("#telnet: got IAC <%d> instead of IAC SE!\n", (int)*s);
                state = NORMAL;
            }
            break;
        }
    }
    CONN_LIST(fd).state = state;
    
    if (!(CONN_LIST(tcp_fd).flags & SPAWN)) {
        log_write(buffer, (char *)p - buffer, 0);
    }
    
    return (char *)p - buffer;
}

void tcp_raw_write __P3 (int,fd, char *,data, int,len)
{
    int i;
    tcp_flush();
    while (len > 0) {
#ifndef MCLIENT
        while ((i = write(fd, data, len)) < 0 && errno == EINTR);
#else
        i = wrapper_tcp_write(fd, data, len);
#endif
        if (i < 0) {
            errmsg("write to socket");
            break;
        }
        data += i;
        len -= i;
    }
}

/*
 * Send current terminal size (RFC 1073)
 */
void tcp_write_tty_size __P0 (void)
{
    static byte buf[] = { IAC, SB, TELOPT_NAWS, 0, 0, 0, 0, IAC, SE };
    
    buf[3] = cols >> 8;
    buf[4] = cols & 0xff;
    buf[5] = lines >> 8;
    buf[6] = lines & 0xff;
    
    tcp_raw_write(tcp_main_fd, (char *)buf, 9);
#ifdef TELOPTS
    tty_printf("[sent term size %d %d]\n", cols, lines);
#endif
}

/*
 * send a string to the main connection on the remote host
 */
void tcp_main_write __P1 (char *,data)
{
    tcp_write(tcp_main_fd, data);
}


static char output_buffer[BUFSIZE];
static int output_len = 0;      /* number of characters in output_buffer */
static int output_socket = -1;  /* to which socket buffer should be sent*/

/*
 * put data in the output buffer for transmission to the remote host
 */
void tcp_write __P2 (int,fd, char *,data)
{
    char *iacs, *out;
    int len, space, iacp;
    len = strlen(data);
    
    if (tcp_main_fd != -1 && tcp_main_fd == fd) {
        if (linemode & LM_NOECHO)
            log_write("", 0, 1); /* log a newline only */
        else
            log_write(data, len, 1);
        reprint_writeline(data);
    }

    /* must be AFTER reprint_writeline() */
    if (CONN_LIST(tcp_fd).flags & SPAWN)
        status(1);
    else
        status(-1);
    
    if (fd != output_socket) { /* is there data to another socket? */
        tcp_flush(); /* then flush it */
        output_socket = fd;
    }
    
    out = output_buffer + output_len;
    space = BUFSIZE - output_len;
    
    while (len) {
        iacs = memchr(data, IAC, len);
        iacp = iacs ? iacs - data : len;
        
        if (iacp == 0) {
            /* we're at the IAC, send it */
            if (space < 2) {
                tcp_flush();
                out = output_buffer;
                space = BUFSIZE;
            }
            *out++ = (char)IAC; *out++ = (char)IAC; output_len += 2; space -= 2;
            data++; len--;
            continue;
        }
        
        while (space < iacp) {
            memcpy(out, data, space);
            data += space; len -= space;
            iacp -= space;
            output_len = BUFSIZE;
            
            tcp_flush();
            out = output_buffer;
            space = BUFSIZE;
        }
            
        if (iacp /* && space >= iacp */ ) {
            memcpy(out, data, iacp);
            out += iacp; output_len += iacp; space -= iacp;
            data += iacp; len -= iacp;
        }
    }
    if (!space) {
        tcp_flush();
        out = output_buffer;
    }
    *out++ = '\n';
    output_len++;
}

/*
 * send all buffered data to the remote host
 */
void tcp_flush __P0 (void)
{
    int n;
    char *p = output_buffer;
    
    if (output_len && output_socket == -1) {
        clear_input_line(1);
        PRINTF("#no open connections. Use '#connect main <address> <port>' to open a connection.\n");
        output_len = 0;
        return;
    }
    
    if (!output_len)
        return;
    
    while (output_len) {
#ifndef MCLIENT
        while ((n = write(output_socket, p, output_len)) < 0 && errno == EINTR);
#else
        n = wrapper_tcp_write(output_socket, p, output_len);
#endif

        if (n < 0) {
            output_len = 0;
            errmsg("write to socket");
            return;
        }
        sent += n;
        p += n;
        output_len -= n;
    }
    
    if (CONN_LIST(output_socket).flags & SPAWN)
        status(1);
    else
        /* sent stuff, so we expect a prompt */
        status(-1);
}

/*
 * Below are multiple-connection support functions:
 */

/*
 * return connection's fd given id,
 * or -1 if null or invalid id is given
 */
int tcp_find __P1 (char *,id)
{
    int i;
    
    for (i=0; i<conn_max_index; i++) {
        if (CONN_INDEX(i).id && !strcmp(CONN_INDEX(i).id, id))
            return CONN_INDEX(i).fd;
    }
    return -1;
}

/*
 * show list of open connections
 */
void tcp_show __P0 (void)
{
    int i = tcp_count+tcp_attachcount;
    
    PRINTF("#%s connection%s opened%c\n", i ? "The following" : "No", 
               i==1 ? " is" : "s are", i ? ':' : '.');
    
    
    for (i=0; i<conn_max_index; i++)
        if (CONN_INDEX(i).id && !(CONN_INDEX(i).flags & SPAWN)) {
            tty_printf("MUD %sactive %s ##%s\t (%s %d)\n",
                       CONN_INDEX(i).flags & ACTIVE ? "   " : "non",
                       i == tcp_main_fd ? "(default)" : "         ",
                       CONN_INDEX(i).id, 
                       CONN_INDEX(i).host, CONN_INDEX(i).port);
        }
    for (i=0; i<conn_max_index; i++)
        if (CONN_INDEX(i).id && (CONN_INDEX(i).flags & SPAWN)) {
            tty_printf("CMD %sactive %s ##%s\t (%s)\n",
                       CONN_INDEX(i).flags & ACTIVE ? "   " : "non",
                       i == tcp_main_fd ? "(default)" : "         ",
                       CONN_INDEX(i).id, CONN_INDEX(i).host);
        }
}

/*
 * permanently change main connection
 */
void tcp_set_main __P1 (int,fd)
{
    /* GH: reset linemode and prompt */
    tcp_main_fd = fd;
    if (linemode & LM_CHAR) {
      linemode = 0;
#ifndef MCLIENT
      tty_special_keys();
#endif
    }
    else
        linemode = 0;
    status(-1);
    reprint_clear();
}

/*
 * open another connection
 */
void tcp_open __P4 (char *,id, char *,initstring, char *,host, int,port)
{
    int newtcp_fd, i;
    
    if (tcp_count+tcp_attachcount >= MAX_CONNECTS) {
        PRINTF("#too many open connections.\n");
        return;
    }
    if (tcp_find(id)>=0) {
        PRINTF("#connection \"%s\" already open.\n", id);
        return;
    }
    
    /* find a free slot */
    for (i=0; i<MAX_CONNECTS; i++) {
        if (!CONN_INDEX(i).id)
            break;
    }
    if (i == MAX_CONNECTS) {
        PRINTF("#internal error, connection table full :(\n");
        return;
    }
        
    if (!(CONN_INDEX(i).host = my_strdup(host))) {
        errmsg("malloc");
        return;
    }
    if (!(CONN_INDEX(i).id = my_strdup(id))) {
        errmsg("malloc");
        free(CONN_INDEX(i).host);
        return;
    }

#ifndef MCLIENT
    /* dial the number by moving the right index in small circles */
    if ((newtcp_fd = tcp_connect(host, port)) < 0) {
        free(CONN_INDEX(i).host);
        free(CONN_INDEX(i).id);
        CONN_INDEX(i).id = 0;
        return;
    }
#else
    wrapper_tcp_connect(host, port, initstring, i);
}

/*
 * This is the slot that Qt will connect to once the connection has been established.
 * This was done in order to prevent a blocking check whether the host existed or not
 */
void wrapper_tcp_connect_slot __P4 (char *,initstring, int,port, int,i, int,newtcp_fd)
{
    if (newtcp_fd < 0) {
        free(CONN_INDEX(i).host);
        free(CONN_INDEX(i).id);
        CONN_INDEX(i).id = 0;
        return;
    }
#endif

    conn_table[newtcp_fd] = i;
    CONN_INDEX(i).flags = ACTIVE;
    CONN_INDEX(i).state = NORMAL;
    CONN_INDEX(i).port = port;
    CONN_INDEX(i).fd = newtcp_fd;

    if (tcp_max_fd < newtcp_fd)
        tcp_max_fd = newtcp_fd;

    if (conn_max_index <= i)
        conn_max_index = i+1;
    
#ifndef MCLIENT
    FD_SET(newtcp_fd, &fdset);          /* add socket to select() set */
#else
    wrapper_tcp_assign_id(CONN_INDEX(i).fd, CONN_INDEX(i).id);
#endif
    tcp_count++;

    if (opt_info && tcp_count) {
      PRINTF("#default connection is now \"%s\"\n", CONN_INDEX(i).id);
    }
    tcp_set_main(tcp_fd = newtcp_fd);
    if (opt_sendsize)
        tcp_write_tty_size();

    if (initstring) {
        parse_instruction(initstring, 0, 0, 1);
        history_done = 0;
    }
}

/*
 * close a connection
 */
void tcp_close __P1 (char *,id)
{
    int i, sfd;
    
    status(1);
    tty_puts(edattrend);
    /*
     * because we may be called from get_remote_input()
     * if tcp_read gets an EOF, before edattrend is
     * printed by get_remote_input() itself.
     */
    
    if (id) {  /* #zap cmd */
        if ((sfd = tcp_find(id)) < 0) {
            tty_printf("#no such connection: \"%s\"\n", id);
            return;
        }
    } else
        sfd = tcp_fd;  /* connection closed by remote host */

#ifndef MCLIENT
    shutdown(sfd, 2);
    close(sfd);
#else
    wrapper_tcp_close_socket(sfd);
#endif

    abort_edit_fd(sfd);
    
    tty_printf("#connection on \"%s\" closed.\n", CONN_LIST(sfd).id);
    
    if (sfd == tcp_main_fd) { /* main connection closed */
        if (tcp_count == 1) { /* was last connection */
            if (opt_exit)
                exit_powwow();
            tty_puts("#no connections left. Type #quit to quit.\n");
            tcp_fd = tcp_main_fd = -1;
        } else {
            /* must find another connection and promote it to main */
            for (i=0; i<conn_max_index; i++) {
                if (!CONN_INDEX(i).id || CONN_INDEX(i).fd == sfd
                    || (CONN_INDEX(i).flags & SPAWN))
                    continue;
                tty_printf("#default connection is now \"%s\"\n", CONN_INDEX(i).id);
                tcp_main_fd = CONN_INDEX(i).fd;
                break;
            }
            if (sfd == tcp_main_fd) {
                tty_printf("#PANIC! internal error in tcp_close()\nQuitting.\n");
                syserr(NULL);
            }
        }
        tcp_set_main(tcp_main_fd);
    }
    
    if (tcp_fd == sfd)
        tcp_fd = -1; /* no further I/O allowed on sfd, as we just closed it */
    
#ifndef MCLIENT
    FD_CLR(sfd, &fdset);
#endif

    if (CONN_LIST(sfd).flags & SPAWN)
        tcp_attachcount--;
    else
        tcp_count--;
    CONN_LIST(sfd).flags = 0;
    CONN_LIST(sfd).state = NORMAL;
    CONN_LIST(sfd).port = 0;
    free(CONN_LIST(sfd).host); CONN_LIST(sfd).host = 0;
    free(CONN_LIST(sfd).id);   CONN_LIST(sfd).id = 0;
    if (CONN_LIST(sfd).fragment) {
        free(CONN_LIST(sfd).fragment);
        CONN_LIST(sfd).fragment = 0;
    }
    
    /* recalculate conn_max_index */
    i = conn_table[sfd];
    if (i+1 == conn_max_index) {
        do {
            i--;
        } while (i>=0 && !CONN_INDEX(i).id);
        conn_max_index = i+1;
    }
    
    /* recalculate tcp_max_fd */
    for (i = tcp_max_fd = 0; i<conn_max_index; i++) {
        if (CONN_INDEX(i).id && tcp_max_fd < CONN_INDEX(i).fd)
            tcp_max_fd = CONN_INDEX(i).fd;
    }
}

/*
 * toggle output display from another connection
 */
void tcp_togglesnoop __P1 (char *,id)
{
    int sfd;
    
    sfd = tcp_find(id);
    if (sfd>=0) {
        CONN_LIST(sfd).flags ^= ACTIVE;
        if (opt_info) {
            PRINTF("#connection %s is now %sactive.\n",
                      CONN_LIST(sfd).id, CONN_LIST(sfd).flags & ACTIVE ? "" : "non");
        }
    } else {
        PRINTF("#no such connection: %s\n", id);
    }
}

#ifndef MCLIENT
void tcp_spawn __P2 (char *,id, char *,cmd)
{
    int i, childpid, sockets[2];

    if (tcp_find(id)>=0) {
        PRINTF("#connection \"%s\" already open.\n", id);
        return;
    }
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0) {
        errmsg("create socketpair");
        return;
    }
    unescape(cmd);
    
    switch (childpid = fork()) {
      case 0:
        /* child */
        close(0); close(1); close(2);
        setsid();
        dup2(sockets[1], 0);
        dup2(sockets[1], 1);
        dup2(sockets[1], 2);
        close(sockets[0]);
        close(sockets[1]);
        execl("/bin/sh", "sh", "-c", cmd, NULL);
        syserr("execl");
        break;
      case -1:
        close(sockets[0]);
        close(sockets[1]);
        errmsg("fork");
        return;
    }
    close(sockets[1]);
    
    /* Again, we don't want children to inherit sockets */
    fcntl(sockets[0], F_SETFD, FD_CLOEXEC);
    
    /* now find a free slot */
    for (i=0; i<MAX_CONNECTS; i++) {
        if (!CONN_INDEX(i).id) {
            conn_table[sockets[0]] = i;
            break;
        }
    }
    if (i == MAX_CONNECTS) {
        PRINTF("#internal error, connection table full :(\n");
        close(sockets[0]);
        return;
    }
    
    if (!(CONN_INDEX(i).host = my_strdup(cmd))) {
        errmsg("malloc");
        close(sockets[0]);
        return;
    }
    if (!(CONN_INDEX(i).id = my_strdup(id))) {
        errmsg("malloc");
        free(CONN_INDEX(i).host);
        close(sockets[0]);
        return;
    }
    CONN_INDEX(i).flags = ACTIVE | SPAWN;
    CONN_INDEX(i).state = NORMAL;
    CONN_INDEX(i).port = 0;
    CONN_INDEX(i).fd = sockets[0];
    
    FD_SET(sockets[0], &fdset);        /* add socket to select() set */
    tcp_attachcount++;
    
    if (tcp_max_fd < sockets[0])
        tcp_max_fd = sockets[0];
    if (conn_max_index <= i)
        conn_max_index = i+1;
    
    if (opt_info) {
        PRINTF("#successfully spawned \"%s\" with pid %d\n", id, childpid);
    }
    
    /*
     * when the child exits we also get an EOF on the socket,
     * so no special care is needed by the SIGCHLD handler.
     */    
}
#endif

